01 - PLAN DU TUTORIEL
=====================
01 - PLAN DU TUTORIEL
02 - INCIPIT
03 - .NET C'EST QUOI CA ENCORE ?
03.00 - C'EST DIFFICILE LE CRACKING .NET ?
04 - KEYGENING
04.00 - SHAKE IT BABY
04.01 - OPOPOP... D'ACCORD MAIS POURQUOI ?
04.02 - ANALYSE DE LA METHODE
04.03 - CODING DU KEYGEN
05 - PATCHING
05.00 - PATCHING BRUT
05.00.01 - LE CODE A MODIFIER
05.00.02 - OU MODIFIER LE CODE ?
05.00.03 - J'AI RIEN COMPRIS ! ON
RECOMMENCE
05.01 - RECOMPILATION
06 - HAPPY END
06.00 - PUSH REALLIFE/RET
02 - INCIPIT
============
Aye aye !
Aujourd'hui nous allons nous attaquer à un petit keygeneme tout
simple mais qui à la particularité d'être un
crackme .NET (C# ou VB.NET on ne peut pas savoir) d'ailleurs voici ce
que nous dit PEiD :
"Microsoft Visual C# / Basic .NET"
Le .NET est quelque chose d'assez nouveau et assez flou (pour certains)
etant donné l'abus d'acronymes utilisé... Cependant, cela
fait parti des choses qu'il faudra tenter de maitriser à
l'avenir.
Je ne vous cache pas que je suis un neophyte dans ce domaine. Ce tuto
peut donc contenir son lot de conneries qui fera sans doute grincer les
dents des plus experimentés... Veuillez m'en excuser.
Bref, nous verrons tout cela plus tard.
Voici ce que nous dit l'ami Neitsa en attendant :) (cf. ForumCrack)
Youp !
Un petit Keygenme (avec patching accepté pour ceux ne souhaitant
pas faire un Keygen).
La difficulté dépend des moyens que vous utilisez pour
l'attaque... La routine est très très simple, le
problème sera sûrement de savoir où elle se trouve
:D
Un petit tut serait pas mal... ;)
Merci à vous. Je releaserais un petit tut de mon coté.
Bon courage.
(...)
Nous allons donc faire le patch ET le keygen (oui je m'ennuie pas mal
ces temps-ci, j'avoue).
Et n'oubliez pas : Ce tutoriel à beau être long, il n'y
à rien de vraiment compliqué !
Allez... Fait peter Simone !
03 - .NET C'EST QUOI CA ENCORE ?
================================
Bonne question, je vais tenter d'y repondre clairement et simplement.
Pas évident dans la mesure ou j'entrave pas au chose au charabia
qu'on peut trouver sur le net.
Ceci est loin d'être une reference, c'est néanmoins le
strict minimum pour comprendre le fonctionnement de ce crackme.
Bon... En fait il y a le Win32 et le .NET.
Le Win32 est composé d'un ensemble de librairies (user32.dll,
kernel32.dll, etc) aussi apellées API. Dans ces APIs sont
stockées des fonctions permettant au programmeur d'accomplir
certaines taches sans avoir à tout coder lui même.
Avec ces fonctions nous pouvons par exemple afficher une message box
(fonction MessageBox dans user32.dll), ouvrir un fichier (OpenFile dans
kernel32.dll), etc, etc...
Le programmeur choisi le langage qu'il souhaite pour produire du code
Win32 (Assembleur, C, C++ etc...). Le code produit par le programmeur
est en suite traduit en langage machine par le compilateur, et est donc
comprehensible par le micro-processeur.
Ca devrait être un fait acquis pour vous, ou bien vous avez
encore besoin de potasser un peu sur le sujet...
Qu'en est-il du .NET maintenant ?
C'est un chouia plus complexe. En fait nous avons le .NET framework qui
fait office d'API ici, mais sous forme d'objets. Je m'explique : Le
.NET framework est une grande librairie (mscoree.dll) contenant
plusieurs namespaces, classes, méthodes etc...
Prenons par exemple le cas de l'affichage d'une message box en C :
MessageBox(hWnd, "Salut", "Wesh", MB_OK);
Cette ligne de code fait appel à la fonction MessageBox contenue
dans la librairie user32.dll.
Voici le même exemple en C# :
System.Windows.Forms.MessageBox.Show("Salut", "Wesh",
MessageBoxButtons.OK);
(Il existe un moyen de simplifier la lisibilité du code mais
cela depasse du cadre de ce tutoriel).
Voici donc ce que nous avons :
System.Windows.Forms.MessageBox.Show
\__________________/ \________/ \__/
|
| |
|
| |
|
| +- La
méthode
|
|
|
+- La classe
|
+- Le namespace
Le namespace contient plusieurs classes. Les classes contiennent
plusieurs méthodes (fonctions), et un tas d'autre trucs
passionants.
Vous avez maintenant une breve idée du fonctionnement du .NET
framework.
Alors qu'en Win32 le code produit par le programmeur était
directement traduit en langage machine (à peu de choses
près), en .NET c'est different. Le code est traduit en IL.
IL pour Intermediate Language (ou MSIL pour Microsoft Intermediate
Language) est un langage intermediaire (comme le Port Salut). C'est
l'equivalent du langage machine pour les programmes .NET. Ce code est
traduit à la volée en langage machine lors de l'execution
du programme.
N'esperez donc pas trouver d'instructions Asm avec votre
desassembleur/debuggeur préféré pauvres mortels.
(Sauf peut-être avec IDA PRO 4.8, mais chez moi il reste muet...)
J'espère avoir été assez clair la dessus, et pas
avoir raconté trop de conneries. J'espère aussi que ma
comparaison avec le Win32 est judicieuse... Qui-sait, peut-être
que dans quelques années les gens se diront "C'est quoi c'truc,
Win32 ?".
03.00 - C'EST DIFFICILE LE CRACKING .NET ?
------------------------------------------
On va voir ça tout de suite :)
04 - KEYGENING
==============
Commencons par le plus simple, le keygen (bien que ça puisse
sembler paradoxal)...
Nous allons pour ça nous armer d'un tool tres util, il s'agit de
Reflector (cf. Liens du site de la NAS, section Outils).
Reflector est un decompilateur pour les programmes .NET. Cela signifie
qu'il sera apte à nous fournir les sources d'un programme .NET,
que ce soit en IL, C#, VB .NET ou Delphi .NET. A l'heure actuelle
(4.1.95.0) il est encore un peu buggé, mais nous nous en
contenterons...
04.00 - SHAKE IT BABY
---------------------
Nous allons donc ici, à l'aide de notre décompilateur,
acceder aux différentes méthodes du programme (en .NET
tout est objetisé, les programmes, les variables, bref... Tout
!), pour pouvoir analyser celles-ci. Nous coderons ensuite notre
keygens à l'aide des informations obtenues.
On lance donc Reflector, une dialgbox peut apparaitre nous demandant la
version du .NET frameword à utiliser, choisisons donc la plus
recente, et c'est parti !
Le programme va donc charger les composants du framework. Une fois que
c'est fait, chargez le crackme dans Reflector.
Un nouvel item 'WindowsApplication1' devrait apparaitre. Developpez le
sous item 'WindowsApplication1.exe' (le module), puis le sous item
'WindowsApplication1' (le namespace), et enfin 'Form1' (la classe).
Parfait... Nous avons maintenant acces aux differentes méthodes
contenues dans le programme, pas trop fatigué ?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
- WindowsApplication1 (assembly)
|
- WindowsApplication1.exe (module)
|
- WindowsApplication1 (namespace)
|
- Form1 (classe)
|
+ button1_Click(Object,
EventArgs) : Void
+ Main() : Void
+ Rev(String) : String
+ D'autres méthodes...
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
Les méthodes sont affichés de la manière suivante
: NomDeLaMethode(Argument1, Argument2, ArgumentN) : TypeDeValeurDeSortie
J'ai pas fait de grandes études mais je suppute que c'est la
méthode 'button1_Click' qui nous interesse. Et effectivement
c'est le cas.
04.01 - OPOPOP... D'ACCORD MAIS POURQUOI ?
------------------------------------------
Pour nous en convaincre il suffit de desassembler la méthode
InitializeComponent() et de jetter un oeil à cet endroit :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
this.button1.Location = new Point(0x6d,
0x80);
this.button1.Name = "button1";
this.button1.TabIndex = 1;
this.button1.Text = "&Verify";
this.button1.Click += new
EventHandler(this.button1_Click);
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
Voici les propriétés de l'objet button1. La
méthode 'Click' de l'objet est associée à
'EventHandler(this.button1_Click)'.
En gros, quand toi y en a cliquer sur le bouton, le programme y en a
appeler la méthode 'button1_Click'...
04.02 - ANALYSE DE LA METHODE
-----------------------------
Voici le code obtenu (en C#) de la méthode 'button1_Click'. Vous
ne rêvez pas... Vous êtes bien en train de cracker le
programme :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
private void button1_Click(object sender, EventArgs e)
{
int num1 = 0;
string text1 = this.textBox1_Name.Text;
if (text1.Length < 5)
// Si nom < 5, on dégage.
{
MessageBox.Show("Name too short");
}
else if (this.textBox2_Pass.Text.Length
< 5) // Si serial < 5, on
dégage
{
MessageBox.Show("Pass too short");
}
else
{
string text4 = text1;
// text4 contient notre nom.
for
(int num3 = 0; num3 < text4.Length; num3++)
{
char ch1 = text4[num3];
int num2 = ch1;
num1 += num2;
// On additionne tous les
chars du nom dans num1.
}
string text2 = text1 + num1;
// Concatenation du nom avec num1 dans text2.
string text3 = this.Rev(text2);
// on appelle la méthode Rev pour
tranformer text2 dans text3.
if
(text3 != this.textBox2_Pass.Text)
// text3 est-il different du serial entré ?
{
MessageBox.Show("BadBoy !");
// Ben oui... Donc on dégage.
}
else
{
MessageBox.Show("GoodBoy !!!"); //
Non... Donc on a gagné !
}
}
}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
Le code se devine facilement pour peu que vous ayez quelques bases de
programmation C (ou C# au mieux).
Si par exemple vous êtes un habitué du Delphi ou Visual
Basic, une petite list box vous permet de choisir le langage
generé. Elle est pas belle la vie ? Si, hein...
On pourait très bien faire un copier/coller de la routine et la
debugger avec le debugger integré à MSVC#... Et c'est ce
que nous allons faire pour la routine suivante... Ceci nous permettra
de mieux appréhender le code.
Etudions donc la méthode Rev, en cliquant dessus on voit
aparaitre son code :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
private string Rev(string str)
{
char[] chArray1 =
str.ToCharArray();
// On tranforme la string en tableau de char.
int num1 = str.Length - 1;
// num1
vaut la longeur de la string moins le NULL Terminating Char.
if (1 == num1)
// Si c'est egal à 1,
{
return str;
// On
retourne la string d'entrée.
}
int num2 = 0;
while (num2 < num1)
{
char[] chArray2;
IntPtr ptr1;
(chArray2 = chArray1)[(int) (ptr1 = (IntPtr) num2)] = chArray2[(int)
ptr1] ^ chArray1[num1];
(chArray2 = chArray1)[(int) (ptr1 = (IntPtr) num1)] = chArray2[(int)
ptr1] ^ chArray1[num2];
(chArray2 = chArray1)[(int) (ptr1 = (IntPtr) num2)] = chArray2[(int)
ptr1] ^ chArray1[num1];
num2++;
num1--;
}
return new string(chArray1);
}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
On crée donc un nouveau projet (Console Application) avec MSVC#
et on colle la routine Rev de manière à avoir le code
suivant.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
using System;
using System.Collections.Generic;
using System.Text;
namespace test
{
class Program
{
static string Rev(string str)
{
char[] chArray1 = str.ToCharArray();
int
num1 = str.Length - 1;
if
(1 == num1)
{
return str;
}
int
num2 = 0;
while (num2 < num1)
{
char[] chArray2;
IntPtr ptr1;
(chArray2 = chArray1)[(int)(ptr1 = (IntPtr)num2)] =
(char)(chArray2[(int)ptr1] ^ chArray1[num1]);
(chArray2 = chArray1)[(int)(ptr1 = (IntPtr)num1)] =
(char)(chArray2[(int)ptr1] ^ chArray1[num2]);
(chArray2 = chArray1)[(int)(ptr1 = (IntPtr)num2)] =
(char)(chArray2[(int)ptr1] ^ chArray1[num1]);
num2++;
num1--;
}
return new string(chArray1);
}
static void Main(string[]
args)
{
Console.WriteLine(Rev("meatreya"));
}
}
}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
Notez que j'ai corrigé le casting qui generé une erreur
dans la méthode 'Rev' : '(char)(chArray2[(int)ptr1] ^
chArray1[num1]);'
J'appelle ensuite ma méthode 'Rev' en lui donnant comme argument
une string. J'affiche ensuite la string de sortie avec la
méthode 'WriteLine'.
Nous savons ceci grace à la signature de la méthode :
'Rev(String) : String'
L'appel à 'Rev' se fait dans la méthode 'Main' de la
classe 'Program', il s'agit de l'entry point du program. C'est à
dire la méthode qui sera executée directement
après le lancement du programme.
Vous pouvez debugger et vous apercevoir que ce que fait la
méthode 'Rev' est une inversion de l'ordre des chars de la
chaine fournie.
04.03 - CODING DU KEYGEN
------------------------
Pour coder ce keygen, rien de plus simple, je vais vous montrer la
marche à suivre.
Tout d'abord voyons ensemble le schéma du fonctionement du
crackme :
Le Crackme :
- Si la longeur du nom < 5 : Nom invalide.
- Si la longeur du serial < 5 : Serial invalide.
- On calcul la somme de tous les chars du nom.
- On concatène la somme obtenue à la suite du nom
("JeanMichel123").
- On inverse l'ordre des charactères de la string
"JeanMichel123".
- Si la string inversée correspond au serial entré par
l'utilisateur : C'est bon.
- Sinon : C'est pas bon.
C'est relativement simple. Maintenant voyons le schéma du
fonctionnement du keygen que nous devrons coder :
Le Keygen :
- Si la longeur du nom < 5 : Nom invalide.
- On calcul la somme de tous les chars du nom.
- On concatène la somme obtenue à la suite du nom
("JeanMichel123").
- On inverse l'ordre des charactères de la string
"JeanMichel123".
- La string inversée est notre serial.
Le code source de mon keygen est le suivant (je reprend mon projet
console precedent) :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
using System;
using System.Collections.Generic;
using System.Text;
namespace test
{
class Program
{
static string Rev(string str)
{
char[] chArray1 = str.ToCharArray();
int
num1 = str.Length - 1;
if
(1 == num1)
{
return str;
}
int
num2 = 0;
while (num2 < num1)
{
char[] chArray2;
IntPtr ptr1;
(chArray2 = chArray1)[(int)(ptr1 = (IntPtr)num2)] =
(char)(chArray2[(int)ptr1] ^ chArray1[num1]);
(chArray2 = chArray1)[(int)(ptr1 = (IntPtr)num1)] =
(char)(chArray2[(int)ptr1] ^ chArray1[num2]);
(chArray2 = chArray1)[(int)(ptr1 = (IntPtr)num2)] =
(char)(chArray2[(int)ptr1] ^ chArray1[num1]);
num2++;
num1--;
}
return new string(chArray1);
}
static void Main(string[]
args)
{
int
a = 0;
Console.Write("KGme by Neitsa[FRET]\nHaXxOr3d by ++Meat\n\nEntrez votre
nom : ");
String Nom = Console.ReadLine();
if
(Nom.Length < 5)
{
Console.WriteLine("Le nom est trop court...\n");
return;
}
for
(int i = 0; i < Nom.Length; i++)
{
a += (int)Nom[i];
}
String Serial = Nom + a;
Console.WriteLine("Le serial est : " + Rev(Serial) + "\n\nA
bientot...\n");
}
}
}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
Je pense que cela se passe de commentaires. Si vous rencontrez quelques
difficultés, renseignez vous sur le langage C#. Soit via des
ouvrages specialisés, soit via des documentations
trouvées sur le net.
L'execution du keygen nous donne ceci :
KGme by Neitsa[FRET]
HaXxOr3d by ++Meat
Entrez votre nom : meatreya
Le serial est : 658ayertaem
A bientot...
Appuyez sur une touche pour continuer...
On test, et ça fonctionne. Qui en aurait douté ?
Passons maintenant au patching du programme. Nous verrons deux
façons différentes de faire cela.
05 - PATCHING
=============
Le patching de ce keygeneme consistera à reverser un saut
conditionnel. Rien de bien excitant me diriez-vous. Pourtant ce nouveau
type de cible nous amène à de nouvelles approches pour
parvenir à nos fins.
Comme je l'ai dit plus haut, nous allons proceder de deux
manières différentes. Commençons par la
première...
05.00 - PATCHING BRUT
---------------------
Ici nous allons voir quel est le code à reverser, ensuite nous
le repèrerons dans le fichier avec un éditeur
héxadécimal classique, puis nous remplacerons la ou les
opcodes necessaires en héxadécimal. Nous verrons les
difficultés recontrées au fur et à mesure...
Revoyons la méthode contenant la verification
GoToGoodBoy/GoToBadBoy, il s'agit de la méthode 'button1_Click'.
Reperons ensuite la-dite vérification :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
private void button1_Click(object sender, EventArgs e)
{
int num1 = 0;
string text1 = this.textBox1_Name.Text;
if (text1.Length < 5)
// Si nom < 5, on dégage.
{
MessageBox.Show("Name too short");
}
else if (this.textBox2_Pass.Text.Length
< 5) // Si serial < 5, on
dégage
{
MessageBox.Show("Pass too short");
}
else
{
string text4 = text1;
// text4 contient notre nom.
for
(int num3 = 0; num3 < text4.Length; num3++)
{
char ch1 = text4[num3];
int num2 = ch1;
num1 += num2;
// On additionne tous les
chars du nom dans num1.
}
string text2 = text1 + num1;
// Concatenation du nom avec num1 dans text2.
string text3 = this.Rev(text2);
// on appelle la méthode Rev pour
tranformer text2 dans text3.
if
(text3 != this.textBox2_Pass.Text)
// text3 est-il different du serial entré ? <-- Hep !
Psst ! Petite fille !
{
MessageBox.Show("BadBoy !");
// Ben oui... Donc on dégage.
}
else
{
MessageBox.Show("GoodBoy !!!"); //
Non... Donc on a gagné !
}
}
}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
La ligne interessante est :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
if
(text3 != this.textBox2_Pass.Text)
// text3 est-il different du serial entré ? <-- Hep !
Psst ! Petite fille !
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
Il suffirait de changer le '!=' en '==' pour valider le serial dans le
cas où celui-ci serait incorrect (le B-A BA du cracking, hu !).
Tout va bien, mais nous avons besoin d'informations
supplémentaires pour mener à bien notre mission. Nous
devons savoir où se situe le code en question, et... Quel est le
code en question ? Etant donné que nous ne pouvons modifier le
code recompilé, nous devrons jouer avec le IL.
05.00.01 - LE CODE A MODIFIER
-----------------------------
Très bien, il y à une listbox en haut de la fenêtre
avec la liste des langages disponibles, choisissons le langage "IL" et
regardons la tournure que notre listing a pris :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
(...)
L_0086: ldfld
[System.Windows.Forms]System.Windows.Forms.TextBox
WindowsApplication1.Form1::textBox2_Pass
L_008b: callvirt instance string
[System.Windows.Forms]System.Windows.Forms.Control::get_Text()
L_0090: call bool
string::op_Inequality(string, string)
L_0095: brfalse.s L_00a3
L_0097: ldstr "BadBoy !"
L_009c: call
[System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
L_00a1: pop
L_00a2: ret
L_00a3: ldstr "GoodBoy !!!"
L_00a8: call
[System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
L_00ad: pop
L_00ae: ret
}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
Même si je ne connais pas le langage IL, ma cervelle me permet de
savoir que ce code correspond grossierement à :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
(...)
if
(text3 != this.textBox2_Pass.Text)
// text3 est-il different du serial entré ? <-- Hep !
Psst ! Petite fille !
{
MessageBox.Show("BadBoy !");
// Ben oui... Donc on dégage.
}
else
{
MessageBox.Show("GoodBoy !!!"); //
Non... Donc on a gagné !
}
}
}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
Donc à notre verification... Interessons-nous à ces
quelques lignes :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
L_0090: call bool
string::op_Inequality(string, string)
L_0095: brfalse.s L_00a3
L_0097: ldstr "BadBoy !"
L_009c: call
[System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
L_00a1: pop
L_00a2: ret
L_00a3: ldstr "GoodBoy !!!"
L_00a8: call
[System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
L_00ad: pop
L_00ae: ret
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
Toujours avec notre cervelle, on peut traduire ce listing en
pseudo-code :
- Verification de l'inegalité des strings (renvoi 'true' =
strings differentes / renvoi 'false' = strings egales)
- Si false, on va à L_00A3 (good boy)
- Affiche "BadBoy !"
- Retour
L_00A3:
- Affiche "GoodBoy !!!"
- Retour
Remplacons donc le 'Si pas egal' (brfalse.s) en 'Si egal' (on suppose
qu'il s'agit de 'brtrue.s'). Pour plus d'information, nous devons
telecharger la documentation sur le IL sur ce site :
"http://msdn.microsoft.com/net/ecma/default.asp"
Il s'agit du fichier : "ECMA-335: CLI Partition III - CIL (word/pdf
zip)"
Faisons parler ce document :
brfalse.<length> - branch on false, null, or zero
+-----------+------------------+---------------------------------------------------------------------+
| Format | Assembly Format |
Description
|
+-----------+------------------+---------------------------------------------------------------------+
| 39 <I4> | brfalse target | branch to
target if value is zero
(false)
|
| 2C <I1> | brfalse.s target | branch to target if
value is zero (false), short
form
|
| 39 <I4> | brnull target | branch
to target if value is null (alias for
brfalse)
|
| 2C <I1> | brnull.s target | branch to target
if value is null (alias for brfalse.s), short form |
| 39 <I4> | brzero target | branch
to target if value is zero (alias for
brfalse)
|
| 2C <I1> | brzero.s target | branch to target
if value is zero (alias for brfalse.s), short form |
+-----------+------------------+---------------------------------------------------------------------+
brtrue.<length> - branch on non-false or non-null
+-----------+------------------+-------------------------------------------------------------------------------------------+
| Format | Assembly Format |
Description
|
+-----------+------------------+-------------------------------------------------------------------------------------------+
| 3A <I4> | brtrue target | branch
to target if value is non-zero
(true)
|
| 2D <I1> | brtrue.s target | branch to target
if value is non-zero (true), short
form
|
| 3A <I4> | brinst target | branch
to target if value is a non-null object reference (alias for
brtrue)
|
| 2D <I1> | brinst.s target | branch to target
if value is a non-null object reference, short form (alias for
brtrue.s) |
+-----------+------------------+-------------------------------------------------------------------------------------------+
L'instruction 'brfalse' effectue un branchement (un saut) à
l'offset '$+<lenght>' (où $ est l'offset actuelle), si la
valeur 'false'.
L'instruction 'brtrue' fait l'inverse, branchement si la valeur vaut
'true'.
Pour connaitre l'opcode que nous devrons patcher il faut trouver le
format de l'instruction 'brtrue' correspondant au format de notre
'brfalse'. Donc :
+-----------+------------------+---------------------------------------------------------------------+
| Format | Assembly Format |
Description
|
+-----------+------------------+---------------------------------------------------------------------+
| 2C <I1> | brfalse.s target | branch to target if
value is zero (false), short
form
|
+-----------+------------------+---------------------------------------------------------------------+
Correspond à :
+-----------+------------------+-------------------------------------------------------------------------------------------+
| Format | Assembly Format |
Description
|
+-----------+------------------+-------------------------------------------------------------------------------------------+
| 2D <I1> | brtrue.s target | branch to target
if value is non-zero (true), short
form
|
+-----------+------------------+-------------------------------------------------------------------------------------------+
Nous devrons donc remplacer le '2C' par un '2D'. De même que nous
remplacions des '74' par des '75' pendant notre enfance :)
Rodriiiiiiiigueeeeeeeeeezeuh ! \o/
05.00.02 - OU MODIFIER LE CODE ?
--------------------------------
Encore faut-il le trouver notre '2C'...
Tout ce que nous savons c'est qu'il se situe en 'L_0095'. Cette adresse
est une offset (en héxadécimal) par rapport au
début de la méthode.
Le saut se situe donc en 'DebutDeLaMethode+95h'.
Trouvons donc l'adresse du debut de la méthode...
Pour ce faire nous allons utiliser le tool CFF Explorer de Ntoskrnl
(disponible dans la section Liens sur le site de la NAS).
Lançons CFF Explorer et chargeons le crackme. Et rendons nous
dans la section 'MetaData Tables', nous voyons apparaitre une liste
contenant differents membres, dont le membre 'Method (6)'.
(Je ne parlerai pas des MetaDatas ni de la structure du format .NET,
reportez-vous à la documentation de Microsoft
"http://msdn.microsoft.com/net/ecma/default.asp", ou mieux, au tutoriel
(The .NET File Format) écrit par Ntoskrnl
"http://pmode.net/USERS/116/Files/dotnetformat.htm" toutefois ce n'est
pas necessaire à la comprehension de la suite du tutoriel).
Nous allons donc eplucher chaques membres de la liste 'Method' à
la recherche de notre 'button1_Click'. Il s'agit de la méthode
numéro '5'.
Voici les informations que nous obtenons :
+-----------+----------+-------+----------+-------------------+
| Member | Offset | Size |
Value |
Meaning |
+-----------+----------+-------+----------+-------------------+
| RVA | 000033C0 | Dword | 00002370
|
|
| ImplFlags | 000033C4 | Word | 0000
|
|
| Flags | 000033C6 | Word |
0081
|
|
| Name | 000033C8 | Word |
0109 | button1_Click |
| Signature | 000033CA | Word | 0027 |
Blob Index |
| ParamList | 000033CC | Word | 0002 |
Param Table Index |
+-----------+----------+-------+----------+-------------------+
La seule chose qui nous interesse est le membre 'RVA' (Relative Virtual
Adress), il nous permettra de trouver le debut de la méthode
dans le fichier.
Pour expliquer ce qu'est une RVA je cite un extrait d'un tutoriel de
Gamera qui devrait sortir prochainement :)
IMAGE BASE : C’est l’adresse à laquelle un fichier
est chargé en mémoire.
RVA : Relative Virtual Address. Ce sont des références
à des adresses en mémoire.
VA : C’est l’adresse de quelque chose en mémoire.
Ces 3 notions ne sont pas forcément évidentes à
assimiler, mais elles restent néanmoins indispensables pour la
suite. En fait, on peut dire qu’une RVA indique une distance
à partir de l’image_base.
Voici un exemple imagé qui sera plus parlant :
Prenons un livre. Considérons que ce livre soit un programme.
Vous décidez d’ouvrir le livre (on va dire
qu’ouvrir la couverture de ce livre est semblable au lancement
d’un programme, il est donc chargé en mémoire) : Le
début de ce livre est l’image_base du programme.
Dans ce livre, il y a un index, et chaque élément
important est référencé dans cet index.
Quand nous cherchons un élément précis nous allons
voir notre index, et ensuite nous nous rendons à la page
indiquée par l’index. Eh bien la ligne qui nous dit
à quelle page est l’élément qu nous
cherchons est l’équivalent d’une RVA. Et la page en
question, c’est la VA. Ensuite, nous allons à la page
indiquée. Pour un programme, c’est pareil : on additionne
l’image_base à la RVA, et nous trouvons l’adresse de
l’élément voulu.
D’où: VA = RVA + IMAGE BASE
Prenons on exemple concret : l’image base du programme test.exe
est 401000h.
La première section de ce programme a pour RVA 1000h.
Donc, la première section sera chargée à
l’adresse 401000h + 1000h, soit 402000h.
Le hic c'est que la RVA désigne un endroit dans le programme
chargé en mémoire. Nous, nous voulons l'endroit
correspondant dans le fichier sur le disque dur...
CFF Explorer comporte un petit convertisseur qui nous sera bien utile :
Address Converter.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
VA : 00402370
RVA : 00002370
File Offset : 00001370
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
Notre offset est donc 1370h.
Donc si on récapitule depuis le début :
DebutDeLaMethode+95h = 1370h+95h = 1405h
On veut remplacer un '2C' par un '2D'.
Or, avec un éditeur héxadécimal, on se rend compte
qu'en 1405h, il n'y à pas de trace de '2C'...
Le problème vient du Method Header. C'est un en-tête se
situant au début de chaques méthodes, et qui contient
quelques informations sur celles-ci. Du coup il faut prendre en compte
la taille du header et l'additionner à notre offset (1405h) pour
obtenir l'endroit exact où se situe notre opcode.
Pour faire plus simple :
DebutDeLaMethode+TailleDuHeader+95h = 1370h+XXXX+095h = YYYY
Le document 'The .NET File Format' nous dis à peu près
les choses suivantes concernant le Method Header (je simplifie pour ne
pas avoir a vous embrouiller d'avantage pour rien).
Il y à deux types de headers, le Tiny Header et le Fat Header.
Le Tiny Header à une taille d'un octet tandis que le Fat Header,
une taille de douze octets (0Ch).
Pour savoir quel type de header notre section comporte, il y à
un flag au debut de celle-ci. Voici les valeurs nous permettant de
determiner de quel header nous avons :
+------------------------+-------+------------------------+
|
Flag
| Value |
Description
|
+------------------------+-------+------------------------+
| CorILMethod_FatFormat | 0x3 | Method header is
fat. |
| CorILMethod_TinyFormat | 0x2 | Method header is tiny. |
+------------------------+-------+------------------------+
Ce flag se situe à la partie basse du premier octet de la
méthode, donc dans notre cas en '1370h' (il s'agit de l'offset,
hein). Un petit passage à l'éditeur
héxadécimal :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
1370h : 13 30 03 00 AF 00 00 00 02 00 00 11 16 0D 02 7B
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
13 = 0x1 + 0x3 nous avons donc un Fat Header (de taille 0Ch donc).
Notre octet se trouve finalement en :
DebutDeLaMethode+TailleDuHeader+95h = 1370h+0Ch+095h = 1411h
Hop, édition héxadécimale :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
1410h : 0A 2C 0C 72 23 01 00 70 28 2B 00 00 0A 26 2A 72
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
Et voila ! On remplace le '2C' (brfalse) par notre '2D' (brtrue) et
c'est CrAxX0r3d !
Attention toutesfois, quand vous testerez vos exploit, à
vérifier que vous avez bien entré un nom et un serial
supérieurs à cinq charactères...
Good, good, good, goodboy ! You make me feel so gooooooooooooooooooood !
You know you make me feel so good.
You know you make me feel so good.
05.00.03 - J'AI RIEN COMPRIS ! ON RECOMMENCE
--------------------------------------------
Oui j'avoue que le fait d'aller à droite à gauche pour
reverser un simple saut conditionnel peut être déroutant.
Donc je résume ce que nous venons de faire :
Avec le décompileur :
- On situe notre saut à reverser dans le listing C# : : "if
(blablabla != trucmuche)"
- On trouve la correspondance dans le listing IL : "brfalse.s"
- On note l'offset du saut conditionnel : "L_0095:"
Avec la documentation IL :
- On sait que notre saut à pour opcode '2C' et que le saut
inverse à pour opcode '2D'.
Avec l'éditeur PE :
- On note la RVA de la méthode contenant notre code : "00002370h"
- On convertie la RVA en Offset : "00002370h -> 00001370h"
Avec l'éditeur héxadécimal :
- On va en 00001370h pour determiner le type de Method Header : "0x3 =
Fat Header -> TailleDuHeader = 0Ch"
- On va à l'offset suivante :
DebutDeLaMethode+TailleDuHeader+Offseth = 00001370h+0Ch+95h = 1411h.
- On remplace le '2C' par le '2D'.
Cric crac, l'affaire est dans le sac.
Vous trouvez sans doute cela un peu fastidueux, et c'est pas tout
à fait faux. Cela-dit si les outils que nous nous servons pour
cracker du .NET effectueraient cette tache à notre place, cela
serait un peu plus simple. Comme voler la sucette de la bouche d'un
enfant ou changer un 'je' en 'jne' ;)
Oh et puis faut pas vous plaindre, au moins on à vu du paysage :)
05.01 - RECOMPILATION
---------------------
Voici une technique je ne n'apprecie pas particulierement, mais
ça vaut le coup d'en parler... Ne serait-ce qu'au moins une fois.
Il s'agit ici d'obtenir un version compilable du code fouri par un
désILeur. Nous re-codons ainsi le programme avec un bête
editeur de texte, et re-compilons le tout.
Pour obtenir un code compilable par le suite nous aurons besoin de IL
DASM (disponible dans le SDK .NET Framework :
"http://www.microsoft.com/downloads/search.aspx?displaylang=en&categoryid=10")
Rien de plus simple, on lance ildasm.exe (C:\Program Files\Microsoft
Visual Studio 8\SDK\v2.0\Bin), on charge le crackme dans le
désILeur et on click sur "File|Dump".
Il n'y à rien a modifier aux options, celles par defaut
convienent.
Nous obtenons donc un fichier '*.il' (je l'ai appelé 'DuMp.il'
car je suis un hellith). Ainsi que deux fichiers '
WindowsApplication1.Form1.resources' et 'DuMp.res'.
Ces fichiers seront necessaire pour la re-compilation, et doivent donc
toujours être dans le même repertoire que notre fichier
'*.il'.
Ouvrons notre fichier avec Notepad, et regardons la ligne suivante :
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
IL_0095: brfalse.s IL_00a3
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
A moi aussi ça me rappel quelque chose... On change "brfalse.s"
par "brtrue.s" et on sauvegarde le fichier.
Ensuite il nous reste plus qu'à compiler le fichier avec la
ligne de commande suivante :
ilasm DuMp.il
Le programme 'ilasm.exe' se trouve dans le repertoire
"C:\WINDOWS\Microsoft.NET\Framework\vX.X.XXXXX\".
Nous obtenons ainsi un fichier "DuMp.exe". On le test et... Oh ! Oui...
C'est cracké :|
Comme dirait l'autre, c'est la porte ouverte à toutes les
fenêtres...
06 - HAPPY END
==============
Nous pouvons conclure ce tutoriel sur ces quelques mots...
(cf.
"http://msdn.microsoft.com/library/fre/default.asp?url=/library/FRE/dncscol/html/csharp01182001.asp").
(...)
Cela nous amène à une autre question sensible : «
S'il est possible à tout le monde de voir mon IL et mes
métadonnées, est-il possible de reconstituer la logique
de mon code ? » La réponse est « Oui, cela est
possible ». La situation n'est guère différente de
ce qui se passait jusqu'à maintenant, puisque du code x86
optimisé pouvait également faire l'objet d'une
reconstitution logique.
La différence réside dans l'effort nécessaire ; il
est beaucoup plus facile de le faire dans .NET qu'avec du code x86
natif. Ceci est un autre problème sur lequel nous travaillons
actuellement.
(...)
Vive le progrès, et vive la mariée :)
06.00 - PUSH REALLIFE/RET
-------------------------
Ainsi s'achève ce tutoriel qui m'a pris 5 jours à
écrire pour ce crackme qui m'a pris 5 minutes à torcher :p
J'espère qu'il servira à quelqu'un, ou au pire qu'il
m'apportera la gloire, l'argent et les femmes (faites un effort) :/
Salut à toi Neitsa (Peering Inside Les Cartons) pour son crackme
qui m'a permis de me pencher sur le sujet.
Salut à toi Ntoskrnl pour son tutoriel sur le format .NET et son
tool CFF Explorer.
Salut à toi NAS, FRET, ForumCrack, FFF, NGEN.
Salut à toi #fret, #uct, #nas (oui enfin il y à pas grand
monde :D), #xXx (Pandemonium à un chan IRC...).
Salut à toi jeune lect(eur/rice) pour avoir lu ce tutoriel
jusqu'au bout (sous les pavets, le savoir).
Salut à toi chienne de vie.
Salut à toi les autres...
Bisoux visqueux.
++Meat^NAS^FRET
^
. / \ .
/___\
.
"Hey nigga, why you fuck with me ?!"
"Fuck you nigga ! Cuz I can !"
- Dr Dre & Ice Cube - Natural Born Killaz